﻿using MetadataDesignerLibrary;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;
using System.Runtime.Versioning;

namespace InstructionEncoderTest
{
    internal class Program
    {
        private static readonly BlobContentId _blobContentId = new BlobContentId(
            new Guid("17E5BDE2-2243-5EAD-ABB3-1002F92068A7"),
            0x04030201);

        private const string _assemblyName = "Output";
        private const string _moduleName = "Output.dll";

        private static void Main(string[] args)
        {
            using (var peStream = new FileStream(_moduleName, FileMode.OpenOrCreate, FileAccess.ReadWrite))
            {
                MetadataDesigner designer = new MetadataDesigner();
                Generate(designer);
                PEHeaderBuilder peHeaderBuilder = new PEHeaderBuilder(
                    imageCharacteristics: Characteristics.ExecutableImage | Characteristics.Dll);
                designer.WritePEImage(
                    peStream,
                    default,
                    peHeaderBuilder,
                    _blobContentId,
                    CorFlags.ILOnly);
            }

            Assembly executingAssembly = Assembly.GetExecutingAssembly();
            TargetFrameworkAttribute targetFrameworkAttribute = executingAssembly.GetCustomAttribute<TargetFrameworkAttribute>();
            Console.WriteLine("[TargetFrameworkAttribute]");
            Console.WriteLine($"FrameworkDisplayName: {targetFrameworkAttribute.FrameworkDisplayName}");
            Console.WriteLine($"FrameworkName: {targetFrameworkAttribute.FrameworkName}");

            Assembly resultAssembly = Assembly.LoadFrom(_moduleName);
            Type type = resultAssembly.GetType($"{_assemblyName}.Program");
            object instance = Activator.CreateInstance(type);
            PropertyInfo prop = type.GetProperty("MyProperty");
            object value = prop.GetValue(instance);
        }

        private static void Generate(MetadataDesigner designer)
        {
            ILRule.WriteRange(
                designer,
                new ModuleDefinitionRule(
                    0,
                    _moduleName,
                    _blobContentId.Guid,
                    default,
                    default),
                new AssemblyDefinitionRule(
                    _assemblyName,
                    new Version(1, 0, 0, 0),
                    null,
                    null,
                    0,
                    AssemblyHashAlgorithm.None));

            AssemblyReferenceHandle corlibAssemblyRef =
                new AssemblyReferenceRule(
                    "System.Runtime",
                    new Version(7, 0, 0, 0),
                    null,
                    new byte[] { 0xb0, 0x3f, 0x5f, 0x7f, 0x11, 0xd5, 0x0a, 0x3a },
                    default,
                    null).Write(designer);

            TypeReferenceHandle systemObjectTypeRef =
                new TypeReferenceRule(
                    corlibAssemblyRef,
                    "System",
                    "Object").Write(designer);

            ILRule.WriteRange(
                designer,
                new TypeDefinitionRule(
                    default,
                    null,
                    "<Module>",
                    default),
                new TypeDefinitionRule(
                    default,
                    null,
                    "<Module-1>",
                    default));

            MethodSignatureEncoder objectCtorMember = designer.MethodSignature(
                new MethodSignatureParameters() { IsInstanceMethod = true },
                enc =>
                {
                    enc.Parameters(0, returnType => returnType.Void(),
                        parameters => { });
                });

            int ctorBodyOffset = designer.MethodBody(
                new MethodBodyParameters(), enc =>
            {
                MemberReferenceHandle objectCtorMemberRef =
                    new MemberReferenceRule(
                        systemObjectTypeRef,
                        ".ctor",
                        objectCtorMember.Builder.ToArray()).Write(designer);

                // ldarg.0
                enc.LoadArgument(0);

                // call instance void [mscorlib]System.Object::.ctor()
                enc.Call(objectCtorMemberRef);

                // ret
                enc.OpCode(ILOpCode.Ret);
            });

            new MethodDefinitionRule(
                MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
                MethodImplAttributes.IL,
                ".ctor",
                objectCtorMember.Builder.ToArray(),
                ctorBodyOffset).Write(designer);

            MethodSignatureEncoder methodSignature = designer.MethodSignature(
                new MethodSignatureParameters(), enc =>
            {
                enc.Parameters(2, returnType => returnType.Type().Double(),
                    parameters =>
                    {
                        parameters.AddParameter().Type().Double();
                        parameters.AddParameter().Type().Double();
                    });
            });

            int methodBodyOffset = designer.MethodBody(
                new MethodBodyParameters(), enc =>
            {
                // Get reference to System.ArgumentOutOfRangeException constructor
                TypeReferenceHandle typeRefHandle =
                    new TypeReferenceRule(
                        corlibAssemblyRef,
                        "System",
                        "ArgumentOutOfRangeException").Write(designer);

                // Signature: .ctor(string)
                MethodSignatureEncoder ctor = designer.MethodSignature(
                    new MethodSignatureParameters() { IsInstanceMethod = true },
                    enc =>
                {
                    enc.Parameters(1, returnType => returnType.Void(),
                        parameters =>
                        {
                            parameters.AddParameter().Type().String();
                        });
                });

                MemberReferenceHandle ctorMemberRef =
                    new MemberReferenceRule(
                        typeRefHandle,
                        ".ctor",
                        ctor.Builder.ToArray()).Write(designer);

                LabelHandle label1 = enc.DefineLabel();
                LabelHandle label2 = enc.DefineLabel();

                // ldarg.0
                enc.OpCode(ILOpCode.Ldarg_0);

                // ldc.r8 0
                enc.LoadConstantR8(0);

                // bge.un.s LABEL1
                enc.Branch(ILOpCode.Bge_un_s, label1);

                // ldstr "length"
                enc.LoadString(designer.Builder.GetOrAddUserString("length"));

                // newobj instance void [System.Runtime]System.ArgumentOutOfRangeException::.ctor(string)
                enc.OpCode(ILOpCode.Newobj);
                enc.Token(ctorMemberRef);

                // throw
                enc.OpCode(ILOpCode.Throw);

                // LABEL1: ldarg.1
                enc.MarkLabel(label1);
                enc.OpCode(ILOpCode.Ldarg_1);

                // ldc.r8 0
                enc.LoadConstantR8(0);

                // bge.un.s LABEL2
                enc.Branch(ILOpCode.Bge_un_s, label2);

                // ldstr "width"
                enc.LoadString(designer.Builder.GetOrAddUserString("width"));

                // newobj instance void [System.Runtime]System.ArgumentOutOfRangeException::.ctor(string)
                enc.OpCode(ILOpCode.Newobj);
                enc.Token(ctorMemberRef);

                // throw
                enc.OpCode(ILOpCode.Throw);

                // LABEL2: ldarg.0
                enc.MarkLabel(label2);
                enc.OpCode(ILOpCode.Ldarg_0);

                // ldarg.1
                enc.OpCode(ILOpCode.Ldarg_1);

                // mul
                enc.OpCode(ILOpCode.Mul);

                // ret
                enc.OpCode(ILOpCode.Ret);
            });

            ILRule.WriteRange(
                designer,
                new ParameterRule(ParameterAttributes.None, "length"),
                new ParameterRule(ParameterAttributes.None, "width"),
                new MethodDefinitionRule(
                    MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig,
                    MethodImplAttributes.IL,
                    "TestMethod",
                    methodSignature.Builder.ToArray(),
                    methodBodyOffset));

            MethodSignatureEncoder methodSignature2 = designer.MethodSignature(
                new MethodSignatureParameters(), enc =>
            {
                enc.Parameters(2, returnType => returnType.Type().Double(),
                    parameters =>
                    {
                        parameters.AddParameter().Type().Double();
                        parameters.AddParameter().Type().Double();
                    });
            });

            int methodBodyOffset2 = designer.MethodBody(
                new MethodBodyParameters(), enc =>
            {
                // Get reference to System.ArgumentOutOfRangeException constructor
                TypeReferenceHandle typeRefHandle =
                    new TypeReferenceRule(
                        corlibAssemblyRef,
                        "System",
                        "ArgumentOutOfRangeException").Write(designer);

                // Signature: .ctor(string)
                MethodSignatureEncoder ctor = designer.MethodSignature(
                    new MethodSignatureParameters() { IsInstanceMethod = true }, enc =>
                {
                    enc.Parameters(1, returnType => returnType.Void(),
                        parameters =>
                        {
                            parameters.AddParameter().Type().String();
                        });
                });

                MemberReferenceHandle ctorMemberRef =
                    new MemberReferenceRule(
                        typeRefHandle,
                        ".ctor",
                        ctor.Builder.ToArray()).Write(designer);

                LabelHandle label1 = enc.DefineLabel();
                LabelHandle label2 = enc.DefineLabel();

                // ldarg.0
                enc.OpCode(ILOpCode.Ldarg_0);

                // ldc.r8 0
                enc.LoadConstantR8(0);

                // bge.un.s LABEL1
                enc.Branch(ILOpCode.Bge_un_s, label1);

                // ldstr "first"
                enc.LoadString(designer.Builder.GetOrAddUserString("first"));

                // newobj instance void [System.Runtime]System.ArgumentOutOfRangeException::.ctor(string)
                enc.OpCode(ILOpCode.Newobj);
                enc.Token(ctorMemberRef);

                // throw
                enc.OpCode(ILOpCode.Throw);

                // LABEL1: ldarg.1
                enc.MarkLabel(label1);
                enc.OpCode(ILOpCode.Ldarg_1);

                // ldc.r8 0
                enc.LoadConstantR8(0);

                // bge.un.s LABEL2
                enc.Branch(ILOpCode.Bge_un_s, label2);

                // ldstr "second"
                enc.LoadString(designer.Builder.GetOrAddUserString("second"));

                // newobj instance void [System.Runtime]System.ArgumentOutOfRangeException::.ctor(string)
                enc.OpCode(ILOpCode.Newobj);
                enc.Token(ctorMemberRef);

                // throw
                enc.OpCode(ILOpCode.Throw);

                // LABEL2: ldarg.0
                enc.MarkLabel(label2);
                enc.OpCode(ILOpCode.Ldarg_0);

                // ldarg.1
                enc.OpCode(ILOpCode.Ldarg_1);

                // mul
                enc.OpCode(ILOpCode.Mul);

                // ret
                enc.OpCode(ILOpCode.Ret);
            });

            ILRule.WriteRange(
                designer,
                new ParameterRule(ParameterAttributes.None, "first"),
                new ParameterRule(ParameterAttributes.None, "second"),
                new MethodDefinitionRule(
                    MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig,
                    MethodImplAttributes.IL,
                    "TestMethod2",
                    methodSignature2.Builder.ToArray(),
                    methodBodyOffset2));

            MethodSignatureEncoder methodSignatureRef =
                designer.MethodSignature(
                    new MethodSignatureParameters(), enc =>
            {
                enc.Parameters(2, returnType => returnType.Type().Double(),
                    parameters =>
                    {
                        parameters.AddParameter().Type().Double();
                        parameters.AddParameter().Type(true).Double();
                    });
            });

            LocalVariablesEncoder locals = designer.LocalVariableSignature(
                new LocalVariableSignatureParameters(3),
                enc =>
            {
                enc.AddVariable().Type().Boolean();
                enc.AddVariable().Type().Boolean();
                enc.AddVariable().Type().Double();
            });

            int methodBodyOffsetRef = designer.MethodBody(new MethodBodyParameters()
            {
                LocalVariablesSignature = designer.Builder.AddStandaloneSignature(
                    designer.Builder.GetOrAddBlob(locals.Builder))
            }, enc =>
            {
                // Get reference to System.ArgumentOutOfRangeException constructor
                TypeReferenceHandle typeRefHandle =
                    new TypeReferenceRule(
                        corlibAssemblyRef,
                        "System",
                        "ArgumentOutOfRangeException").Write(designer);

                // Signature: .ctor(string)
                MethodSignatureEncoder ctor = designer.MethodSignature(
                    new MethodSignatureParameters() { IsInstanceMethod = true },
                    enc =>
                {
                    enc.Parameters(1, returnType => returnType.Void(),
                        parameters =>
                        {
                            parameters.AddParameter().Type().String();
                        });
                });

                MemberReferenceHandle ctorMemberRef =
                    new MemberReferenceRule(
                        typeRefHandle,
                        ".ctor",
                        ctor.Builder.ToArray()).Write(designer);

                LabelHandle label1 = enc.DefineLabel();
                LabelHandle label2 = enc.DefineLabel();
                LabelHandle label3 = enc.DefineLabel();

                // if (first < 0.0)
                enc.LoadArgument(0);
                enc.LoadConstantR8(0.0);
                enc.OpCode(ILOpCode.Clt);
                enc.StoreLocal(0);
                enc.LoadLocal(0);
                enc.Branch(ILOpCode.Brfalse_s, label1);

                // throw new ArgumentOutOfRangeException("first");
                enc.LoadString(designer.Builder.GetOrAddUserString("first"));
                enc.OpCode(ILOpCode.Newobj);
                enc.Token(ctorMemberRef);
                enc.OpCode(ILOpCode.Throw);

                // if (second < 0.0)
                enc.MarkLabel(label1);
                enc.LoadArgument(1);
                enc.OpCode(ILOpCode.Ldind_r8);
                enc.LoadConstantR8(0.0);
                enc.OpCode(ILOpCode.Clt);
                enc.StoreLocal(1);
                enc.LoadLocal(1);
                enc.Branch(ILOpCode.Brfalse_s, label2);

                // throw new ArgumentOutOfRangeException("second");
                enc.LoadString(designer.Builder.GetOrAddUserString("second"));
                enc.OpCode(ILOpCode.Newobj);
                enc.Token(ctorMemberRef);
                enc.OpCode(ILOpCode.Throw);

                // return first * second;
                enc.MarkLabel(label2);
                enc.LoadArgument(0);
                enc.LoadArgument(1);
                enc.OpCode(ILOpCode.Ldind_r8);
                enc.OpCode(ILOpCode.Mul);
                enc.StoreLocal(2);
                enc.Branch(ILOpCode.Br_s, label3);

                enc.MarkLabel(label3);
                enc.LoadLocal(2);
                enc.OpCode(ILOpCode.Ret);
            });

            new ParameterRule(ParameterAttributes.None, "first").Write(designer);
            new ParameterRule(ParameterAttributes.None, "second").Write(designer);

            new MethodDefinitionRule(
                MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig,
                MethodImplAttributes.IL,
                "TestMethod3",
                methodSignatureRef.Builder.ToArray(),
                methodBodyOffsetRef).Write(designer);

            SignatureTypeEncoder fieldDignature = designer.FieldSignature(enc =>
            {
                enc.Double();
            });

            FieldDefinitionHandle fieldDef = new FieldDefinitionRule(
                FieldAttributes.Public | FieldAttributes.Static,
                "MyField",
                fieldDignature.Builder.ToArray()).Write(designer);

            SignatureTypeEncoder propFieldDignature = designer.FieldSignature(enc =>
            {
                enc.Int64();
            });

            FieldDefinitionHandle propFieldDef = new FieldDefinitionRule(
                FieldAttributes.Private | FieldAttributes.InitOnly,
                "<MyProperty>k__BackingField",
                propFieldDignature.Builder.ToArray()).Write(designer);

            #region Property Definition 1

            MethodSignatureEncoder propEncoder = designer.PropertySignature(
                new PropertySignatureParameters(true), enc =>
                {
                    enc.Parameters(
                        0,
                        returnType => returnType.Type().Int64(),
                        parameters => { });
                });

            MethodSignatureEncoder propGetterSignature = designer.MethodSignature(
                new MethodSignatureParameters()
                {
                    IsInstanceMethod = true
                },
                enc =>
                {
                    enc.Parameters(
                        0,
                        returnType => returnType.Type().Int64(),
                        parameters => { });
                });

            int propGetterBody = designer.MethodBody(
                new MethodBodyParameters(),
                enc =>
                {
                    enc.LoadConstantI8(123);
                    enc.OpCode(ILOpCode.Ret);
                });

            PropertyDefinitionHandle propHandle = new PropertyDefinitionRule(
                PropertyAttributes.None,
                "MyProperty",
                propEncoder.Builder.ToArray()).Write(designer);

            MethodDefinitionHandle propGetterHandle = new MethodDefinitionRule(
                MethodAttributes.PrivateScope | MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName,
                MethodImplAttributes.IL,
                "get_MyProperty",
                propGetterSignature.Builder.ToArray(),
                propGetterBody).Write(designer);

            designer.Builder.AddMethodSemantics(
                propHandle,
                MethodSemanticsAttributes.Getter,
                propGetterHandle);

            #endregion Property Definition 1

            #region Property Definition 2

            MethodSignatureEncoder propEncoder2 = designer.PropertySignature(
                new PropertySignatureParameters(true), enc =>
                {
                    enc.Parameters(
                        0,
                        returnType => returnType.Type().Double(),
                        parameters => { });
                });

            MethodSignatureEncoder propGetterSignature2 = designer.MethodSignature(
                new MethodSignatureParameters()
                {
                    IsInstanceMethod = true
                },
                enc =>
                {
                    enc.Parameters(
                        0,
                        returnType => returnType.Type().Double(),
                        parameters => { });
                });

            int propGetterBody2 = designer.MethodBody(
                new MethodBodyParameters(),
                enc =>
                {
                    enc.LoadConstantR8(55.0);
                    enc.OpCode(ILOpCode.Ret);
                });

            PropertyDefinitionHandle propHandle2 = new PropertyDefinitionRule(
                PropertyAttributes.None,
                "MyProperty2",
                propEncoder2.Builder.ToArray()).Write(designer);

            MethodDefinitionHandle propGetterHandle2 = new MethodDefinitionRule(
                MethodAttributes.PrivateScope | MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName,
                MethodImplAttributes.IL,
                "get_MyProperty2",
                propGetterSignature2.Builder.ToArray(),
                propGetterBody2).Write(designer);

            designer.Builder.AddMethodSemantics(
                propHandle2,
                MethodSemanticsAttributes.Getter,
                propGetterHandle2);

            #endregion Property Definition 2

            TypeDefinitionHandle programType = new TypeDefinitionRule(
                TypeAttributes.AutoLayout | TypeAttributes.AnsiClass | TypeAttributes.Class | TypeAttributes.Public | TypeAttributes.BeforeFieldInit,
                _assemblyName,
                "Program",
                systemObjectTypeRef).Write(designer);

            new TypeDefinitionRule(
                default,
                null,
                "<Module-2>",
                default).Write(designer);
        }
    }
}